Skip to main content

Tree

  • Tree problems usually just spin around DFS or BFS and utilizes some specific tree properties so it's important to be able to implement these 2 algo while blindfolded

  • DFS

    • The idea is usually to use recursion and recursively traverse the left and right subtree while performing some operation
    • Sometimes it's helpful to use another params in the recursive function to keep track of some data (eg: path sum, current node path, etc)
  • Preorder Traversal

def preorderTraversal(self, root: Optional[TreeNode]) -> List[int]:
if not root:
return []

left = self.preorderTraversal(root.left)
right = self.preorderTraversal(root.right)

return [root.val] + left + right
  • **Inorder Traversal
def inorderTraversal(self, root: Optional[TreeNode]) -> List[int]:
if not root:
return []

left = self.inorderTraversal(root.left)
right = self.inorderTraversal(root.right)

return left + [root.val]+ right
  • **Postorder Traversal
def postorderTraversal(self, root: Optional[TreeNode]) -> List[int]:
if not root:
return []

left = self.postorderTraversal(root.left)
right = self.postorderTraversal(root.right)

return left + right + [root.val]
def buildTree(self, preorder: List[int], inorder: List[int]) -> Optional[TreeNode]:
if not preorder or not inorder:
return None

root = TreeNode(preorder[0])
# We can use rootIndex of inorder because
# preorder: root + left + right
# inorder: left + root + right
# So we can use the rootIndex to split the preorder and inoder to 2 subarrays
rootIndex = inorder.index(root.val)
root.left = self.buildTree(preorder[1:rootIndex+1], inorder[:rootIndex+1])
root.right = self.buildTree(preorder[rootIndex+1:], inorder[rootIndex+1:])

return root
# https://leetcode.com/problems/path-sum/
def hasPathSum(self, root: Optional[TreeNode], targetSum: int) -> bool:
def getSum(node, curSum):
if not node:
return False
if not node.left and not node.right and curSum + node.val == targetSum:
return True

left = getSum(node.left, curSum + node.val)
right = getSum(node.right, curSum + node.val)

return left or right

return getSum(root, 0)
  • BFS
    • The idea is to use queue to keep track of what to traverse next (we can use queue/stack for DFS too but recursion is usually cleaner)
# https://leetcode.com/problems/binary-tree-level-order-traversal/
def levelOrder(self, root: Optional[TreeNode]) -> List[List[int]]:
res = []
if not root:
return res
queue = [root]

while queue:
cur_level = []
cur_len = len(queue)
for i in range(cur_len):
node = queue.pop(0)
cur_level.append(node.val)
if node.left:
queue.append(node.left)
if node.right:
queue.append(node.right)
res.append(cur_level)

return res

NOTES

  • An important property when dealing with Full Binary Tree is that it only has 2 childrens, from this we can calculate that:

    Binary Tree in Array Representation If a binary tree is represented as an array:

    1. **Index 0** represents the root node.
    2. For a node at index i:
    • **Left Child**: The left child is located at index 2i + 1.
    • **Right Child**: The right child is located at index 2i + 2.
  • Sometimes it is helpful to think of a Tree as a [[Graph]], and we can create an adjacency list from a Tree and then traverse it like a Graph or apply Graph algorithm.

Variants

Binary Search Tree

  • Definition: A tree where each node has at most two children, and for any node

    • All values in the left subtree are less than the node's value
    • All values in the right subtree are greater than the node's value
    • In-order traversal yields sorted order
  • Efficeint for search, insert, and delete operations when balanced: O(log n)

  • These are some of the fundamental problems that demonstrate BST properties

def sortedArrayToBST(self, nums: List[int]) -> Optional[TreeNode]:
if not nums:
return

mid = len(nums) // 2
root = TreeNode(nums[mid])
root.left = self.sortedArrayToBST(nums[:mid])
root.right = self.sortedArrayToBST(nums[mid+1:])

return root

https://leetcode.com/problems/validate-binary-search-tree

     def isValidBST(self, root: Optional[TreeNode]) -> bool:
def helper(node, lower, upper):
if not node:
return True
if node.val >= upper or node.val <= lower:
return False

return helper(node.left, lower, node.val) and helper(node.right, node.val, upper)

return helper(root, float('-inf'), float('inf'))

https://leetcode.com/problems/insert-into-a-binary-search-tree

 def insertIntoBST(self, root: Optional[TreeNode], val: int) -> Optional[TreeNode]:
if not root:
return TreeNode(val)

if val > root.val:
root.right = self.insertIntoBST(root.right, val)

if val < root.val:
root.left = self.insertIntoBST(root.left, val)

return root

https://leetcode.com/problems/delete-node-in-a-bst

  • Important algorithm to memorize
  • 3 steps:
    1. Find the node to remove
    2. Replace the node by its successor (either smallest in right subtree or largest in left subtree)
    3. Remove the successor
def deleteNode(self, root: Optional[TreeNode], key: int) -> Optional[TreeNode]:
def findSuccessor(node):
if node.left:
return findSuccessor(node.left)
return node

if not root:
return

if key > root.val:
root.right = self.deleteNode(root.right, key)
elif key < root.val:
root.left = self.deleteNode(root.left, key)
else:
if not root.left and not root.right:
return None
elif not root.left:
return root.right
elif not root.right:
return root.left
else:
successor = findSuccessor(root.right)
root.val = successor.val
root.right = self.deleteNode(root.right, root.val)

return root

Lowest Common Ancestor

https://leetcode.com/discuss/interview-question/6024811

https://leetcode.com/problems/lowest-common-ancestor-of-a-binary-tree-ii

  • Use the helper/dfs function to both find the common ancestor and the existence of each node
  • Maintain 2 variables to keep track of the existence of each node
def lowestCommonAncestor(self, root: 'TreeNode', p: 'TreeNode', q: 'TreeNode') -> 'TreeNode':
foundP = False
foundQ = False

def helper(node):
nonlocal foundP
nonlocal foundQ
if not node:
return

left = helper(node.left)
right = helper(node.right)

if node.val == p.val:
foundP = True
return node
if node.val == q.val:
foundQ = True
return node

if left and right:
return node
return left or right

res = helper(root)
return res if foundP and foundQ else None